
//
// Copyright 2009 Arne Halvorsen (arne.halvorsen@gmail.com)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

// Project page: http://code.google.com/p/mf4j/

//
// 091116a- aha42 - Starts in line change tracking.
// 091116b- aha42 - checkType(String ).
// 091118 - aha42 - getIntAttribute(String), getIntAttribute(String, int) and
//                  getIntMandatoryAttribute(String name).
// 091121a- aha42 - hasValue() and hasAttribute(String).
// 091121b- aha42 - Added project page link (google code) to header comment.
// 091210 - aha42 - IOException -> FlickrException.
//

package org.aha.mf4j.response;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.aha.mf4j.FlickrException;
import org.aha.mf4j.util.Constants;
import org.aha.mf4j.visitor.Visitor;
import org.aha.mf4j.visitor.PrintVisitor;

/**
 * <p>
 *   Represents objects from a parsed Flickr API response.
 * </p>
 * @author Arne Halvorsen (aha42).
 */
public final class FlickrObject 
{
  private final String m_type;
  
  private final FlickrObject m_parent;
  
  private Map<String, String> m_attrs=null;
  
  private Map<String, List<FlickrObject>> m_colls=null;
  
  private String m_value=null;
  
  /**
   * <p>
   *   Creates root.
   * </p>
   * @param type {@link #getType()}.
   */
  FlickrObject(String type)
  {
    if (type==null)
    {
      throw new NullPointerException("type");
    }
    
    m_type=type;
    m_parent=null;
  }
  
  /**
   * <p>
   *   Creates internal.
   * </p>
   * @param type   {@link #getType()}.
   * @param parent Parent.
   */
  FlickrObject(String type, FlickrObject parent)
  {
    if (type==null)
    {
      throw new NullPointerException("type");
    }
    if (parent==null)
    {
      throw new NullPointerException("parent");
    }
    
    m_type=type;
    m_parent=parent;
  }
  
  /**
   * <p>
   *   Gets parent.
   * </p>
   * @return Parent or {@code null} if {@code this} is root.
   */
  public FlickrObject getParent(){ return m_parent; }
  
  /**
   * <p>
   *   Gets this type.
   * </p>
   * @return Type.
   */
  public String getType(){ return m_type; }
  
  /**
   * <p>
   *   Asserts {@code this} is of given
   *   {@link #getType() type}.
   * </p>
   * @param type Type asserted.
   * @throws IOException If {@code getType().equals(type)==false}.
   */
  public void assertType(String type) throws FlickrException
  {
    if (type==null)
    {
      throw new NullPointerException("type");
    }
    
    if (!m_type.equals(type))
    {
      throw new FlickrException("excpected element of type : "+type+
         " found element of type : "+m_type);
    }
  }
  
  /**
   * <p>
   *   Similar to
   *   {@link #assertType(String)} but throws run time exception.
   * </p>
   * @param type Type asserted.
   * @throws IllegalArgumentException If {@code getType().equals(type)==false}.
   */
  public void checkType(String type)
  {
    if (type==null)
    {
      throw new NullPointerException("type");
    }
    
    if (!m_type.equals(type))
    {
      throw new IllegalArgumentException("excpected element of type : "+type+
         " found element of type : "+m_type);
    }
  }
  
  /**
   * <p>
   *   Sets value.
   * </p>
   * <p>
   *   This is the value found as {@code cdata} in the XML response.
   * </p>
   * @param val Value.
   */
  public void setValue(String val)
  {
    if (val==null)
    {
      throw new NullPointerException("val");
    }
    
    m_value=val;
  }
  
  /**
   * <p>
   *   Tells if has value.
   * </p>
   * @return {@code true} if has, {@code false} if has not.
   * @see #getValue()
   * @see #getMandatoryValue()
   */
  public boolean hasValue(){ return m_value!=null; }
  
  /**
   * <p>
   *   Gets value.
   * </p>
   * <p>
   *   This is the value found as {@code cdata} in the XML response.
   * </p> 
   * @return Value or {@code null} if has no value.
   */
  public String getValue(){ return m_value; }
  
  /**
   * <p>
   *   Gets mandatory value.
   * </p>
   * <p>
   *   This is the value found as {@code cdata} in the XML response.
   * </p> 
   * @return Value.
   * @throws FlickrException If has no value.
   */
  public String getMandatoryValue() throws FlickrException
  {
    String retVal=getValue();
    if (retVal==null)
    {
      throw new FlickrException("misses mandatory value (cdata) attribute "+
        "associated with : "+this);
    }
    return retVal;
  }
  
  /**
   * <p>
   *   Sets attribute.
   * </p>
   * @param name  Attribute name.
   * @param value Attribute value.
   */
  public void setAttribute(String name, String value)
  {
    if (name==null)
    {
      throw new NullPointerException("name");
    }
    if (value==null)
    {
      throw new NullPointerException("value");
    }
    
    if (m_attrs==null) m_attrs=new HashMap<String, String>();
    m_attrs.put(name, value);
  }
  
  /**
   * <p>
   *   Tells if has attribute.
   * </p>
   * @param name Attribute name.
   * @return {@code true} if has, {@code false} if has not.
   */
  public boolean hasAttribute(String name)
  {
    if (name==null)
    {
      throw new NullPointerException("name");
    }
    
    return m_attrs==null ? false : m_attrs.containsKey(name);
  }
  
  /**
   * <p>
   *   Gets attribute.
   * </p>
   * @param name Attribute name.
   * @return Value or {@code null} if not found.
   */
  public String getAttribute(String name)
  {
    if (name==null)
    {
      throw new NullPointerException("name");
    }
    
    return m_attrs==null ? null : m_attrs.get(name);
  }
  
  /**
   * <p>
   *   Gets mandatory attribute.
   * </p>
   * @param name Attribute name.
   * @return Value.
   * @throws FlickrException If not found.
   */
  public String getMandatoryAttribute(String name) throws FlickrException
  {
    String retVal=getAttribute(name);
    if (retVal==null)
    {
      throw new FlickrException("misses mandatory "+name+
        " attribute associated with : "+this);
    }
    return retVal;
  }
  
  /**
   * <p>
   *   Gets {@code int} attribute.
   * </p>
   * @param name Attribute name.
   * @param def  Default value.
   * @return Value.
   * @throws NumberFormatException If attribute value can not be parsed as an 
   *         {@code int}. 
   */
  public int getIntAttribute(String name, int def)
  {
    String val=getAttribute(name);
    return val==null ? def : Integer.parseInt(val);
  }
  
  /**
   * <p>
   *   Gets {@code int} attribute.
   * </p>
   * @param name Attribute name.
   * @return Value or {@code null} if not found.
   * @throws NumberFormatException If attribute value can not be parsed as an 
   *         {@code int}. 
   */
  public Integer getIntAttribute(String name)
  {
    String val=getAttribute(name);
    return (val==null) ? null : new Integer(val);
  }
  
  /**
   * <p>
   *   Gets mandatory {@code int} attribute.
   * </p>
   * @param name Attribute name.
   * @return Value.
   * @throws NumberFormatException If attribute value can not be parsed as an 
   *         {@code int}.
   * @throws FlickrException If not found. 
   */
  public int getMandatoryIntAtrribute(String name) throws FlickrException
  {
    return Integer.parseInt(getMandatoryAttribute(name));
  }
  
  /**
   * <p>
   *   Gets names on attributes {@code this} has.
   * </p>
   * @return Names.
   */
  public String[] getAttributeNames()
  {
    return m_attrs==null ? Constants.E_STRING_A : 
                           m_attrs.keySet().toArray(Constants.E_STRING_A);
  }
  
  /**
   * <p>
   *   Adds child
   *   {@code FlickrObject} of {@code this}.
   * </p>
   * @param child Child to add.
   */
  public void addChild(FlickrObject child)
  {
    if (child==null)
    {
      throw new NullPointerException("fo");    
    }
    
    if (m_colls==null) m_colls=new HashMap<String, List<FlickrObject>>();
    
    String type=child.getType();
    List<FlickrObject> coll=m_colls.get(type);
    if (coll==null)
    {
      coll=new ArrayList<FlickrObject>();
      m_colls.put(type, coll);
    }
    coll.add(child);
  }
  
  /**
   * <p>
   *   Gets children of given
   *   {@link #getType() type} {@code this} may have.
   * </p>
   * @param type Child type.
   * @return Objects, the empty array will be returned if none found.
   */
  public FlickrObject[] getChildren(String type)
  {
    if (type==null)
    {
      throw new NullPointerException("type");
    }
    
    List<FlickrObject> coll=(m_colls==null) ? null : m_colls.get(type);
    return coll==null ? E_ARRAY : coll.toArray(E_ARRAY);
  }
  
  /**
   * <p>
   *   Convenient method to get single child of given type. 
   * </p>
   * @param type Child type.
   * @return Found or {@code null} if not found.
   */
  public FlickrObject getChild(String type)
  {
    if (type==null)
    {
      throw new NullPointerException("type");
    }
    
    if (m_colls==null) return null;
    
    List<FlickrObject> coll=m_colls.get(type);
    int n=(coll==null) ? 0 : coll.size();
    return n==0 ? null : coll.get(0);
  }
  
  /**
   * <p>
   *   Convenient method to get single mandatory child of given type. 
   * </p>
   * @param type Child type.
   * @return Found.
   * @throws FlickrException If not found.
   */
  public FlickrObject getMandatoryChild(String type) throws FlickrException
  {
    FlickrObject retVal=getChild(type);
    if (retVal==null)
    {
      throw new FlickrException("misses mandatory "+type+" child of : "+this);
    }
    return retVal;
  }
  
  /**
   * <p>
   *   Gets content types of children {@code this} has.
   * </p>
   * <p>
   *   Using a type value returned by this as argument to 
   *   {@link #getChildren(String)} will result in a non empty array
   *   result.
   * </p>
   * @return Types.
   */
  public String[] getChildTypes()
  {
    return m_colls==null ? Constants.E_STRING_A : 
                           m_colls.keySet().toArray(Constants.E_STRING_A);
  }
  
  @Override
  public String toString() 
  {
    StringBuilder sb=new StringBuilder();
    
    sb.append("type=").append(m_type);
    
    if (m_value!=null) sb.append(",value=").append(m_value);
    
    if (m_attrs!=null && m_attrs.size()>0)
    {
      sb.append(",attributes=[");
      boolean first=true;
      Iterator<Map.Entry<String, String>> attrs=m_attrs.entrySet().iterator();
      while (attrs.hasNext())
      {
        Map.Entry<String, String> entry=attrs.next();
        if (!first) sb.append(",");
        first=false;
        sb.append(entry.getKey()).append('=').append(entry.getValue());
      }
      sb.append("]");
    }
    
    return sb.toString();
  }
  
  /**
   * <p>
   *   Prints hierarchy {@code this} is root in to
   *   {@link System#out standard out}.
   * </p>
   */
  public void printHierarchy(){ visit(PrintVisitor.INSTANCE); }
  
  /**
   * <p>
   *   Prints 
   *   {@code FlickrObject} hierarchies to
   *   {@link System#out standard out}.   
   * </p>
   * @param forest Roots in trees to print.
   */
  public static void printHierarchies(FlickrObject[] forest)
  {
    visit(forest, PrintVisitor.INSTANCE);
  }
  
  /**
   * <p>
   *   Visit hierarchy from {@code this} root.
   * </p>
   * @param visitor Visitor.
   */
  public void visit(Visitor visitor)
  {
    visitor.start();
    visit(0, visitor);
    visitor.end();
  }
  
  /**
   * <p>
   *   Invoked from 
   *   {@link #visit(Visitor)}.
   * </p>
   * @param level   Level we are visiting at.
   * @param visitor Visitor.
   * @param {@code true} if to visit anymore subtrees, {@code false} if done.
   */
  private boolean visit(int level, Visitor visitor)
  {
    if (!visitor.visit(level, this)) return false;
    if (m_colls!=null)
    {
      level++;
      Iterator<Map.Entry<String, List<FlickrObject>>> childs=
        m_colls.entrySet().iterator();
      while (childs.hasNext())
      {
        Map.Entry<String, List<FlickrObject>> curr=childs.next();
        Iterator<FlickrObject> objects=curr.getValue().iterator();
        while (objects.hasNext()) objects.next().visit(level, visitor);
      }
    }
    return true;
  }
  
  /**
   * <p>
   *   Visits a 
   *   {@link FlickrObject} hierarchies.
   * </p>
   * @param forest  Roots in trees to visit.
   * @param visitor Visitor.
   */
  public static void visit(FlickrObject[] forest, Visitor visitor)
  {
    if (forest==null)
    {
      throw new NullPointerException("forest");
    }
    if (visitor==null)
    {
      throw new NullPointerException("visitor");
    }
    
    visitor.start();
    
    int n=forest.length;
    for (int i=0; i<n; i++)
    {
      FlickrObject root=forest[i];
      if (root==null)
      {
        throw new NullPointerException("forest["+i+"]");
      }
      
      if (!root.visit(0, visitor)) 
      {
        visitor.end();
        return;
      }
    }
    
    visitor.end();
  }
  
  /**
   * <p>
   *   Empty array shared.
   * </p>
   */
  public static final FlickrObject[] E_ARRAY=new FlickrObject[0];
  
}
